﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Netcached.Hash;
using System.Configuration;

namespace Netcached
{
    /// <summary>
    /// Class providing connectivity to memcached servers.
    /// </summary>
    public class MemcachedClient : IDisposable
    {
        ///<summary>
        ///If the total size of a serialized object (measured in bytes) exceeds this value, it will be compressed before being stored.
        ///Use value 0 to disable compression.
        ///By default compression is disabled.
        ///</summary>
        public int CompressionThreshold
        {
            get { return serializer.CompressionThreshold; }
            set { serializer.CompressionThreshold = value; }
        }

        ///<summary>
        ///If compression is enabled, this property determines the compression algorithm to be used before an object is stored.
        ///This property can be changed mid-operation and it will not break deserialization of fetched values as compression type is stored together with each value.
        ///</summary>
        public CompressionAlgorithm CompressionAlgorithm
        {
            get { return serializer.CompressionAlgorithm; }
            set { serializer.CompressionAlgorithm = value; }
        }

        private ServerCluster serverCluster;
        private Serializer serializer;
        private bool disposed;

        public MemcachedClient(string serverAddress) : this(new ServerDefinition(serverAddress)) { }
        public MemcachedClient(string serverAddress, int port) : this(new ServerDefinition(serverAddress, port)) { }
        public MemcachedClient(string serverAddress, int port, int sendReceiveTimeout, int maxSocketsPerServer)
            : this(new ServerDefinition(serverAddress, port, sendReceiveTimeout), maxSocketsPerServer) { }

        public MemcachedClient(ServerDefinition server) : this(new ServerDefinition[] { server }) { }
        public MemcachedClient(ServerDefinition server, int maxSocketsPerServer) : this(new ServerDefinition[] { server }, maxSocketsPerServer) { }

        public MemcachedClient(IEnumerable<ServerDefinition> servers) : this(servers, 5) { }
        public MemcachedClient(IEnumerable<ServerDefinition> servers, int maxSocketsPerServer)
        {
            serverCluster = new ServerCluster(servers, maxSocketsPerServer);
            serializer = new Serializer();
        }

        /// <summary>
        ///  This method creates and configures a new instances of MemcachedClient class from the application configuration file. 
        ///  For xml configuration syntax, see the documentation.
        /// </summary>
        /// <returns>New instance of MemcachedClient class.</returns>
        public static MemcachedClient CreateFromConfiguration()
        {
            try
            {
                var serviceConfigSection = ConfigurationManager.GetSection("netcached") as Netcached.Configuration.NetcachedConfigurationSection;

                if (serviceConfigSection.Servers == null || serviceConfigSection.Servers.Count == 0)
                {
                    throw new ConfigurationErrorsException("At least one server must be defined in the configuration file.");
                }

                var servers = new List<ServerDefinition>();
                foreach (var server in serviceConfigSection.Servers)
                {
                    var serverEntry = server as Netcached.Configuration.ServerEntry;

                    var serverDefinition = new ServerDefinition(serverEntry.Address);

                    if (serverEntry.Port != -1)
                        serverDefinition.Port = serverEntry.Port;

                    if (serverEntry.SendReceiveTimeout != -1)
                        serverDefinition.SendReceiveTimeout = serverEntry.SendReceiveTimeout;

                    servers.Add(serverDefinition);
                }

                int maxSocketsPerServer = serviceConfigSection.MaxSocketsPerServer;

                var client = new MemcachedClient(servers, maxSocketsPerServer > 0 ? maxSocketsPerServer : 5);

                client.CompressionAlgorithm = serviceConfigSection.CompressionAlgorithm;
                client.CompressionThreshold = serviceConfigSection.CompressionThreshold;

                return client;
            }
            catch (Exception ex)
            {
                throw new ConfigurationErrorsException("An error occurred while attempting to configure the client", ex);
            }
        }

        #region Store commands

        /// <summary>
        /// This method will unconditionally store an item under the given key.
        /// Corresponds to 'set' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Set(string key, object objectToStore) { return Set(key, objectToStore, 0); }

        /// <summary>
        /// This method will unconditionally store an item under the given key.
        /// Corresponds to 'set' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Set(string key, object objectToStore, int expirationTime)
        {
            var result =  SendStorageCommand("set", key, expirationTime, objectToStore, null);
            return result == StoreResult.Stored;
        }

        /// <summary>
        /// This method will store an item under the given key only if the key is not already in use.
        /// Corresponds to 'add' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Add(string key, object objectToStore) { return Add(key, objectToStore, 0); }

        /// <summary>
        /// This method will store an item under the given key only if the key is not already in use.
        /// Corresponds to 'add' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Add(string key, object objectToStore, int expirationTime)
        {
            var result = SendStorageCommand("add", key, expirationTime, objectToStore, null);
            return result == StoreResult.Stored;
        }

        /// <summary>
        /// This method will replace an item under the given key with a new one only if the key already exists.
        /// Corresponds to 'add' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Replace(string key, object objectToStore) { return Replace(key, objectToStore, 0); }

        /// <summary>
        /// This method will replace an item under the given key with a new one only if the key already exists.
        /// Corresponds to 'add' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Replace(string key, object objectToStore, int expirationTime)
        {
            var result = SendStorageCommand("replace", key, expirationTime, objectToStore, null);
            return result == StoreResult.Stored;
        }

        /// <summary>
        /// This method will append data to an existing value stored under the given key only if the key already exists.
        /// Corresponds to 'append' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Append(string key, object objectToStore) { return Append(key, objectToStore, 0); }

        /// <summary>
        /// This method will append data to an existing value stored under the given key only if the key already exists.
        /// Corresponds to 'append' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Append(string key, object objectToStore, int expirationTime)
        {
            var result = SendStorageCommand("append", key, expirationTime, objectToStore, null);
            return result == StoreResult.Stored;
        }

        /// <summary>
        /// This method will prepend data to an existing value stored under the given key only if the key already exists.
        /// Corresponds to 'prepend' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Prepend(string key, object objectToStore) { return Prepend(key, objectToStore, 0); }

        /// <summary>
        /// This method will prepend data to an existing value stored under the given key only if the key already exists.
        /// Corresponds to 'prepend' method in the memcached specification.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool Prepend(string key, object objectToStore, int expirationTime)
        {
            var result = SendStorageCommand("prepend", key, expirationTime, objectToStore, null);
            return result == StoreResult.Stored;
        }

        /// <summary>
        /// This method perform a 'check-and-set' operation for the given key. It will store an item only if the key already exists and the item has not been modified since the last fetch.
        /// Corresponds to 'cas' method in the memcached specification.
        /// </summary>
        /// <returns>Stored in case of success, ErrorModified if the value has been modified, NotFound if the key does not exist. </returns>
        public CasResult CheckAndSet(string key, object objectToStore, ulong casUnique) { return CheckAndSet(key, objectToStore, 0, casUnique); }

        /// <summary>
        /// This method perform a 'check-and-set' operation for the given key. It will store an item only if the key already exists and the item has not been modified since the last fetch.
        /// Corresponds to 'cas' method in the memcached specification.
        /// </summary>
        /// <returns>Stored in case of success, ErrorModified if the value has been modified, NotFound if the key does not exist. </returns>
        public CasResult CheckAndSet(string key, object objectToStore, int expirationTime, ulong casUnique)
        {
            var result = SendStorageCommand("cas", key, expirationTime, objectToStore, casUnique);

            CasResult casResult = CasResult.ErrorNotFound;
            switch (result)
            {
                case StoreResult.Stored:
                    casResult = CasResult.Stored;
                    break;
                case StoreResult.CASModified:
                    casResult = CasResult.ErrorModified;
                    break;
                case StoreResult.CASNotFound:
                    casResult = CasResult.ErrorNotFound;
                    break;
                default:
                    break;
            }

            return casResult;
        }

        private StoreResult SendStorageCommand(string command, string key, int expirationTime, object objectToStore, ulong? casUnique)
        {
            string commandString = "{0} {1} {2} {3} {4}";
            var serializationResult = serializer.Serialize(objectToStore);

            ValidateKey(key);
            commandString = string.Format(commandString, command, key, serializationResult.SerializationFlags, expirationTime, serializationResult.Bytes.Length);

            if (command == "cas")
            {
                commandString += " " + casUnique.Value;
            }

            commandString += "\r\n";
            var socket = GetSocket(key);

            socket.Send(commandString);
            socket.Send(serializationResult.Bytes);
            socket.Send("\r\n");

            string response = socket.ReadTextLine().Replace("\0", "");
            StoreResult storeResult;

            switch (response)
            {
                case "STORED":
                    storeResult = StoreResult.Stored;
                    break;
                case "NOT_STORED":
                    storeResult = StoreResult.NotStored;
                    break;
                case "EXISTS":
                    storeResult = StoreResult.CASModified;
                    break;
                case "NOT_FOUND":
                    storeResult = StoreResult.CASNotFound;
                    break;
                default:
                    throw new MemcachedException("Received a malformed response after a store command was issued. Response: " + response);
            }

            socket.ReturnToPool();
            return storeResult;
        }

        #endregion

        #region Retrieval commands

        /// <summary>
        /// This method will get the value stored under the given key. It will throw an exception in case of an invalid cast (wrong generic parameter being used).
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>Item in case of success, null if the item does not exist.</returns>
        public T Get<T>(string key)
        {
            ulong casUnique = 0;
            return Get<T>(key, out casUnique);
        }

        /// <summary>
        /// This method will get the value stored under the given key. It will throw an exception in case of an invalid cast (wrong generic parameter being used).
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>Item in case of success, null if the item does not exist.</returns>
        public T Get<T>(string key, out ulong casUnique)
        {
            object item = Get(key, true, out casUnique);

            if (item == null && typeof(T).IsValueType)
            {
                return default(T);
            }

            return (T)item;
        }

        /// <summary>
        /// This method will get the value stored under the given key.
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>Item in case of success, null if the item does not exist.</returns>
        public object Get(string key)
        {
            ulong casUnique = 0;
            return Get(key, false, out casUnique);
        }

        /// <summary>
        /// This method will get the value stored under the given key.
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>Item in case of success, null if the item does not exist.</returns>
        public object Get(string key, bool getCasUnique, out ulong casUnique)
        {
            ValidateKey(key);

            object item = null;
            casUnique = 0;
            string commandString = string.Format("{0} {1}\r\n", getCasUnique ? "gets" : "get", key);

            var socket = GetSocket(key);
            socket.Send(commandString);
            var itemList = ParseGetResult(socket);

            if (itemList != null && itemList.Count == 1)
            {
                item = itemList[0].Item;
                casUnique = itemList[0].CasUnique;
            }

            return item;
        }

        /// <summary>
        /// This method will get values for multiple keys.
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>List of items for all found keys. The collection is never null.</returns>
        public List<RetrievedItem> Get(string[] keys)
        {
            return Get(keys, false);
        }

        /// <summary>
        /// This method will get values for multiple keys.
        /// Corresponds to 'get' method in the memcached specification.
        /// </summary>
        /// <returns>List of items for all found keys. The collection is never null.</returns>
        public List<RetrievedItem> Get(string[] keys, bool getCasUnique)
        {
            List<RetrievedItem> items = new List<RetrievedItem>();
            string commandStringInit = getCasUnique ? "gets" : "get";

            if (keys == null)
                throw new ArgumentNullException("Key(s) for the 'get' command are null");

            Dictionary<string, SocketWrapper> keySocketMap = GetSocketsForKeys(keys);
            Dictionary<SocketWrapper, string> socketCommandStringMap = new Dictionary<SocketWrapper, string>();

            // build all get(s) command strings and assign them to particular sockets
            foreach (var item in keySocketMap)
            {
                if (!socketCommandStringMap.ContainsKey(item.Value))
                {
                    socketCommandStringMap[item.Value] = commandStringInit;
                }

                string commandString = socketCommandStringMap[item.Value]; 
                commandString += " " + item.Key;
                socketCommandStringMap[item.Value] = commandString;
            }

            // get command string for every socket, send all commands and read results in a separate loop
            foreach (var item in socketCommandStringMap)
            {
                SocketWrapper socket = item.Key;
                string commandString = item.Value + "\r\n";
                socket.Send(commandString);
            }

            // read results (this is in a separate loop so that reading results doesn't block other sockets from sending data)
            foreach (var item in socketCommandStringMap)
            {
                SocketWrapper socket = item.Key;
                var retrievedItems = ParseGetResult(socket);
                items.AddRange(retrievedItems);
            }

            return items;
        }

        private List<RetrievedItem> ParseGetResult(SocketWrapper socket)
        {
            List<RetrievedItem> items = new List<RetrievedItem>();

            //VALUE <key> <flags> <bytes> [<cas unique>]\r\n
            bool foundEnd = false;
            while (!foundEnd)
            {
                string itemDeclaration = socket.ReadTextLine();
                if (itemDeclaration.StartsWith("VALUE"))
                {
                    var splitValues = itemDeclaration.Split(' ');

                    string key = splitValues[1];
                    //SerializedType expectedType = (SerializedType)Enum.Parse(typeof(SerializedType), splitValues[2]);
                    int serializationFlags = int.Parse(splitValues[2]);
                    int expectedBytes = int.Parse(splitValues[3]);
                    ulong casUnique = splitValues.Length == 5 ? ulong.Parse(splitValues[4]) : 0;

                    var readBytes = socket.ReadData(expectedBytes);
                    object deserializedItem = serializer.Deserialize(readBytes, serializationFlags);

                    items.Add(new RetrievedItem(key, deserializedItem, casUnique));
                }
                else if (itemDeclaration.StartsWith("END"))
                {
                    foundEnd = true;
                }
            }

            return items;
        }

        #endregion

        #region Delete and flush commands

        /// <summary>
        /// This method will delete a value for the given key.
        /// Corresponds to 'delete' method in the memcached specification.
        /// </summary>
        /// <returns>True in case of success, false if the key does not exist.</returns>
        public bool Delete(string key)
        {
            bool deleted = false;

            ValidateKey(key);
            string commandString = string.Format("delete {0}\r\n", key);

            var socket = GetSocket(key);

            socket.Send(commandString);
            string response = socket.ReadTextLine();
            deleted = response == "DELETED";

            socket.ReturnToPool();
            return deleted;
        }

        /// <summary>
        /// This method will flush all entries on all servers.
        /// Corresponds to 'flush_all' method in the memcached specification.
        /// </summary>
        /// <returns>The method always returns true unless an error occurred.</returns>
        public bool FlushAllEntries()
        {
            return FlushAllEntries(0);
        }

        /// <summary>
        /// This method will flush all entries on all servers.
        /// Corresponds to 'flush_all' method in the memcached specification.
        /// </summary>
        /// <param name="expirationTime">Delay (in seconds) after which all entries will get flushed.</param>
        /// <returns>The method always returns true unless an error occurred.</returns>
        public bool FlushAllEntries(int expirationTime)
        {
            var sockets = GetSocketsForAllServers();

            string commandString = "flush_all";
            if (expirationTime > 0)
            {
                commandString += " " + expirationTime;
            }
            commandString += "\r\n";

            foreach (var socket in sockets)
            {
                socket.Send(commandString);
            }

            bool responseOk = true;
            foreach (var socket in sockets)
            {
                string response = socket.ReadTextLine();
                responseOk = responseOk && response != null && response.StartsWith("OK");
                socket.ReturnToPool();
            }

            return responseOk;
        }

        #endregion

        #region Increment/Decrement, InitializeCounter

        /// <summary>
        /// This method initializes the counter for Increment/Decrement methods. This method must be called before Increment/Decrement is used.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool InitializeCounter(string key, ulong value) { return InitializeCounter(key, value, 0); }

        /// <summary>
        /// This method initializes the counter for Increment/Decrement methods. This method must be called before Increment/Decrement is used.
        /// </summary>
        /// <returns>True if the operation succeeded, false otherwise.</returns>
        public bool InitializeCounter(string key, ulong value, int expirationTime)
        {
            return Set(key, value.ToString(), expirationTime);
        }

        /// <summary>
        /// This method increments a counter by the given value.
        /// Corresponds to 'incr' method in the memcached specification.
        /// </summary>
        /// <returns>New value of the counter if it exists, null otherwise.</returns>
        public ulong? Increment(string key, ulong value)
        {
            return IncrementDecrement("incr", key, value);
        }

        /// <summary>
        /// This method decrements a counter by the given value.
        /// Corresponds to 'decr' method in the memcached specification.
        /// </summary>
        /// <returns>New value of the counter if it exists, null otherwise.</returns>
        public ulong? Decrement(string key, ulong value)
        {
            return IncrementDecrement("decr", key, value);
        }

        private ulong? IncrementDecrement(string command, string key, ulong value)
        {
            ulong? returnValue = null;
            ValidateKey(key);

            string commandString = string.Format("{0} {1} {2}\r\n", command, key, value);
            var socket = GetSocket(key);

            socket.Send(commandString);
            string response = socket.ReadTextLine();

            ulong parsedValue;
            if (ulong.TryParse(response, out parsedValue))
            {
                returnValue = parsedValue;
            }

            socket.ReturnToPool();
            return returnValue;
        }

        #endregion

        #region Touch

        /// <summary>
        /// This method is used to update the expiration time of an existing item without fetching it.
        /// Corresponds to 'touch' method in the memcached specification.
        /// </summary>
        /// <returns>True if the item was found and successfully updated, false otherwise.</returns>
        public bool Touch(string key, int expirationTime)
        {
            ValidateKey(key);

            string commandString = string.Format("touch {0} {1}\r\n", key, expirationTime);
            var socket = GetSocket(key);

            socket.Send(commandString);
            string response = socket.ReadTextLine();

            socket.ReturnToPool();
            return response != null && (response.StartsWith("TOUCHED") || response.StartsWith("STORED"));
        }

        #endregion

        private static ulong ComputeHashForObject(byte[] bytes)
        {
            return HashAlgorithm.HashFNV1a64bits(bytes);
        }

        private void ValidateKey(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentException("Key is null or empty.");
            }
            if (key.Length > 250)
            {
                throw new ArgumentException("Key is longer than 250 characters.");
            }
            if (key.Contains(" "))
            {
                throw new ArgumentException("Key contains whitespace characters.");
            }

            foreach (char c in key)
            {
                if (c < 33 || c > 126)
                {
                    throw new ArgumentException("Key contains control characters.");

                }
            }
        }

        private SocketWrapper GetSocket(string objectKey)
        {
            return serverCluster.GetSocketForKey(objectKey);
        }

        private List<SocketWrapper> GetSocketsForAllServers()
        {
            return serverCluster.GetSocketsForAllServers();
        }

        private Dictionary<string, SocketWrapper> GetSocketsForKeys(string[] keys)
        {
            return serverCluster.GetSocketsForKeys(keys);
        }

        /// <summary>
        /// Synonimous with Dispose.
        /// </summary>
        public void Close()
        {
            Dispose();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    serverCluster.Dispose();
                }

                disposed = true;
            }
        }
    }
}
